/*
 *    Copyright 2009-2012 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ReflectPermission;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.ibatis.reflection.invoker.GetFieldInvoker;
import org.apache.ibatis.reflection.invoker.Invoker;
import org.apache.ibatis.reflection.invoker.MethodInvoker;
import org.apache.ibatis.reflection.invoker.SetFieldInvoker;
import org.apache.ibatis.reflection.property.PropertyNamer;

/*
 * This class represents a cached set of class definition information that
 * allows for easy mapping between property names and getter/setter methods.
 */
/**
 * @author Clinton Begin
 */
public class Reflector {

	private static boolean classCacheEnabled = true;
	private static final String[] EMPTY_STRING_ARRAY = new String[0];
	private static final Map<Class<?>, Reflector> REFLECTOR_MAP = new ConcurrentHashMap<Class<?>, Reflector>();

	private Class<?> type;
	private String[] readablePropertyNames = EMPTY_STRING_ARRAY;
	private String[] writeablePropertyNames = EMPTY_STRING_ARRAY;
	private Map<String, Invoker> setMethods = new HashMap<String, Invoker>();
	private Map<String, Invoker> getMethods = new HashMap<String, Invoker>();
	private Map<String, Class<?>> setTypes = new HashMap<String, Class<?>>();
	private Map<String, Class<?>> getTypes = new HashMap<String, Class<?>>();
	private Constructor<?> defaultConstructor;

	private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>();

	private Reflector(Class<?> clazz) {
		type = clazz;
		addDefaultConstructor(clazz);
		addGetMethods(clazz);
		addSetMethods(clazz);
		addFields(clazz);
		readablePropertyNames = getMethods
				.keySet()
				.toArray(
						new String[getMethods
								.keySet()
								.size()]);
		writeablePropertyNames = setMethods
				.keySet()
				.toArray(
						new String[setMethods
								.keySet()
								.size()]);
		for (String propName : readablePropertyNames) {
			caseInsensitivePropertyMap
					.put(propName
							.toUpperCase(Locale.ENGLISH),
							propName);
		}
		for (String propName : writeablePropertyNames) {
			caseInsensitivePropertyMap
					.put(propName
							.toUpperCase(Locale.ENGLISH),
							propName);
		}
	}

	private void addDefaultConstructor(
			Class<?> clazz) {
		Constructor<?>[] consts = clazz
				.getDeclaredConstructors();
		for (Constructor<?> constructor : consts) {
			if (constructor
					.getParameterTypes().length == 0) {
				if (canAccessPrivateMethods()) {
					try {
						constructor
								.setAccessible(true);
					} catch (Exception e) {
						// Ignored. This is only a final precaution, nothing we
						// can do.
					}
				}
				if (constructor
						.isAccessible()) {
					this.defaultConstructor = constructor;
				}
			}
		}
	}

	private void addGetMethods(
			Class<?> cls) {
		Map<String, List<Method>> conflictingGetters = new HashMap<String, List<Method>>();
		Method[] methods = getClassMethods(cls);
		for (Method method : methods) {
			String name = method
					.getName();
			if (name.startsWith("get")
					&& name.length() > 3) {
				if (method
						.getParameterTypes().length == 0) {
					name = PropertyNamer
							.methodToProperty(name);
					addMethodConflict(
							conflictingGetters,
							name,
							method);
				}
			} else if (name
					.startsWith("is")
					&& name.length() > 2) {
				if (method
						.getParameterTypes().length == 0) {
					name = PropertyNamer
							.methodToProperty(name);
					addMethodConflict(
							conflictingGetters,
							name,
							method);
				}
			}
		}
		resolveGetterConflicts(conflictingGetters);
	}

	private void resolveGetterConflicts(
			Map<String, List<Method>> conflictingGetters) {
		for (String propName : conflictingGetters
				.keySet()) {
			List<Method> getters = conflictingGetters
					.get(propName);
			Iterator<Method> iterator = getters
					.iterator();
			Method firstMethod = iterator
					.next();
			if (getters.size() == 1) {
				addGetMethod(propName,
						firstMethod);
			} else {
				Method getter = firstMethod;
				Class<?> getterType = firstMethod
						.getReturnType();
				while (iterator
						.hasNext()) {
					Method method = iterator
							.next();
					Class<?> methodType = method
							.getReturnType();
					if (methodType
							.equals(getterType)) {
						throw new ReflectionException(
								"Illegal overloaded getter method with ambiguous type for property "
										+ propName
										+ " in class "
										+ firstMethod
												.getDeclaringClass()
										+ ".  This breaks the JavaBeans "
										+ "specification and can cause unpredicatble results.");
					} else if (methodType
							.isAssignableFrom(getterType)) {
						// OK getter type is descendant
					} else if (getterType
							.isAssignableFrom(methodType)) {
						getter = method;
						getterType = methodType;
					} else {
						throw new ReflectionException(
								"Illegal overloaded getter method with ambiguous type for property "
										+ propName
										+ " in class "
										+ firstMethod
												.getDeclaringClass()
										+ ".  This breaks the JavaBeans "
										+ "specification and can cause unpredicatble results.");
					}
				}
				addGetMethod(propName,
						getter);
			}
		}
	}

	private void addGetMethod(
			String name, Method method) {
		if (isValidPropertyName(name)) {
			getMethods.put(name,
					new MethodInvoker(
							method));
			getTypes.put(name, method
					.getReturnType());
		}
	}

	private void addSetMethods(
			Class<?> cls) {
		Map<String, List<Method>> conflictingSetters = new HashMap<String, List<Method>>();
		Method[] methods = getClassMethods(cls);
		for (Method method : methods) {
			String name = method
					.getName();
			if (name.startsWith("set")
					&& name.length() > 3) {
				if (method
						.getParameterTypes().length == 1) {
					name = PropertyNamer
							.methodToProperty(name);
					addMethodConflict(
							conflictingSetters,
							name,
							method);
				}
			}
		}
		resolveSetterConflicts(conflictingSetters);
	}

	private void addMethodConflict(
			Map<String, List<Method>> conflictingMethods,
			String name, Method method) {
		List<Method> list = conflictingMethods
				.get(name);
		if (list == null) {
			list = new ArrayList<Method>();
			conflictingMethods.put(
					name, list);
		}
		list.add(method);
	}

	private void resolveSetterConflicts(
			Map<String, List<Method>> conflictingSetters) {
		for (String propName : conflictingSetters
				.keySet()) {
			List<Method> setters = conflictingSetters
					.get(propName);
			Method firstMethod = setters
					.get(0);
			if (setters.size() == 1) {
				addSetMethod(propName,
						firstMethod);
			} else {
				Class<?> expectedType = getTypes
						.get(propName);
				if (expectedType == null) {
					throw new ReflectionException(
							"Illegal overloaded setter method with ambiguous type for property "
									+ propName
									+ " in class "
									+ firstMethod
											.getDeclaringClass()
									+ ".  This breaks the JavaBeans "
									+ "specification and can cause unpredicatble results.");
				} else {
					Iterator<Method> methods = setters
							.iterator();
					Method setter = null;
					while (methods
							.hasNext()) {
						Method method = methods
								.next();
						if (method
								.getParameterTypes().length == 1
								&& expectedType
										.equals(method
												.getParameterTypes()[0])) {
							setter = method;
							break;
						}
					}
					if (setter == null) {
						throw new ReflectionException(
								"Illegal overloaded setter method with ambiguous type for property "
										+ propName
										+ " in class "
										+ firstMethod
												.getDeclaringClass()
										+ ".  This breaks the JavaBeans "
										+ "specification and can cause unpredicatble results.");
					}
					addSetMethod(
							propName,
							setter);
				}
			}
		}
	}

	private void addSetMethod(
			String name, Method method) {
		if (isValidPropertyName(name)) {
			setMethods.put(name,
					new MethodInvoker(
							method));
			setTypes.put(
					name,
					method.getParameterTypes()[0]);
		}
	}

	private void addFields(
			Class<?> clazz) {
		Field[] fields = clazz
				.getDeclaredFields();
		for (Field field : fields) {
			if (canAccessPrivateMethods()) {
				try {
					field.setAccessible(true);
				} catch (Exception e) {
					// Ignored. This is only a final precaution, nothing we can
					// do.
				}
			}
			if (field.isAccessible()) {
				if (!setMethods
						.containsKey(field
								.getName())) {
					// issue #379 - removed the check for final because JDK 1.5
					// allows
					// modification of final fields through reflection
					// (JSR-133). (JGB)
					// pr #16 - final static can only be set by the classloader
					int modifiers = field
							.getModifiers();
					if (!(Modifier
							.isFinal(modifiers) && Modifier
							.isStatic(modifiers))) {
						addSetField(field);
					}
				}
				if (!getMethods
						.containsKey(field
								.getName())) {
					addGetField(field);
				}
			}
		}
		if (clazz.getSuperclass() != null) {
			addFields(clazz
					.getSuperclass());
		}
	}

	private void addSetField(Field field) {
		if (isValidPropertyName(field
				.getName())) {
			setMethods
					.put(field
							.getName(),
							new SetFieldInvoker(
									field));
			setTypes.put(
					field.getName(),
					field.getType());
		}
	}

	private void addGetField(Field field) {
		if (isValidPropertyName(field
				.getName())) {
			getMethods
					.put(field
							.getName(),
							new GetFieldInvoker(
									field));
			getTypes.put(
					field.getName(),
					field.getType());
		}
	}

	private boolean isValidPropertyName(
			String name) {
		return !(name.startsWith("$")
				|| "serialVersionUID"
						.equals(name) || "class"
					.equals(name));
	}

	/*
	 * This method returns an array containing all methods declared in this
	 * class and any superclass. We use this method, instead of the simpler
	 * Class.getMethods(), because we want to look for private methods as well.
	 * 
	 * @param cls The class
	 * 
	 * @return An array containing all methods in this class
	 */
	private Method[] getClassMethods(
			Class<?> cls) {
		HashMap<String, Method> uniqueMethods = new HashMap<String, Method>();
		Class<?> currentClass = cls;
		while (currentClass != null) {
			addUniqueMethods(
					uniqueMethods,
					currentClass
							.getDeclaredMethods());

			// we also need to look for interface methods -
			// because the class may be abstract
			Class<?>[] interfaces = currentClass
					.getInterfaces();
			for (Class<?> anInterface : interfaces) {
				addUniqueMethods(
						uniqueMethods,
						anInterface
								.getMethods());
			}

			currentClass = currentClass
					.getSuperclass();
		}

		Collection<Method> methods = uniqueMethods
				.values();

		return methods
				.toArray(new Method[methods
						.size()]);
	}

	private void addUniqueMethods(
			HashMap<String, Method> uniqueMethods,
			Method[] methods) {
		for (Method currentMethod : methods) {
			if (!currentMethod
					.isBridge()) {
				String signature = getSignature(currentMethod);
				// check to see if the method is already known
				// if it is known, then an extended class must have
				// overridden a method
				if (!uniqueMethods
						.containsKey(signature)) {
					if (canAccessPrivateMethods()) {
						try {
							currentMethod
									.setAccessible(true);
						} catch (Exception e) {
							// Ignored. This is only a final precaution, nothing
							// we can do.
						}
					}

					uniqueMethods
							.put(signature,
									currentMethod);
				}
			}
		}
	}

	private String getSignature(
			Method method) {
		StringBuilder sb = new StringBuilder();
		Class<?> returnType = method
				.getReturnType();
		if (returnType != null) {
			sb.append(
					returnType
							.getName())
					.append('#');
		}
		sb.append(method.getName());
		Class<?>[] parameters = method
				.getParameterTypes();
		for (int i = 0; i < parameters.length; i++) {
			if (i == 0) {
				sb.append(':');
			} else {
				sb.append(',');
			}
			sb.append(parameters[i]
					.getName());
		}
		return sb.toString();
	}

	private static boolean canAccessPrivateMethods() {
		try {
			SecurityManager securityManager = System
					.getSecurityManager();
			if (null != securityManager) {
				securityManager
						.checkPermission(new ReflectPermission(
								"suppressAccessChecks"));
			}
		} catch (SecurityException e) {
			return false;
		}
		return true;
	}

	/*
	 * Gets the name of the class the instance provides information for
	 * 
	 * @return The class name
	 */
	public Class<?> getType() {
		return type;
	}

	public Constructor<?> getDefaultConstructor() {
		if (defaultConstructor != null) {
			return defaultConstructor;
		} else {
			throw new ReflectionException(
					"There is no default constructor for "
							+ type);
		}
	}

	public Invoker getSetInvoker(
			String propertyName) {
		Invoker method = setMethods
				.get(propertyName);
		if (method == null) {
			throw new ReflectionException(
					"There is no setter for property named '"
							+ propertyName
							+ "' in '"
							+ type
							+ "'");
		}
		return method;
	}

	public Invoker getGetInvoker(
			String propertyName) {
		Invoker method = getMethods
				.get(propertyName);
		if (method == null) {
			throw new ReflectionException(
					"There is no getter for property named '"
							+ propertyName
							+ "' in '"
							+ type
							+ "'");
		}
		return method;
	}

	/*
	 * Gets the type for a property setter
	 * 
	 * @param propertyName - the name of the property
	 * 
	 * @return The Class of the propery setter
	 */
	public Class<?> getSetterType(
			String propertyName) {
		Class<?> clazz = setTypes
				.get(propertyName);
		if (clazz == null) {
			throw new ReflectionException(
					"There is no setter for property named '"
							+ propertyName
							+ "' in '"
							+ type
							+ "'");
		}
		return clazz;
	}

	/*
	 * Gets the type for a property getter
	 * 
	 * @param propertyName - the name of the property
	 * 
	 * @return The Class of the propery getter
	 */
	public Class<?> getGetterType(
			String propertyName) {
		Class<?> clazz = getTypes
				.get(propertyName);
		if (clazz == null) {
			throw new ReflectionException(
					"There is no getter for property named '"
							+ propertyName
							+ "' in '"
							+ type
							+ "'");
		}
		return clazz;
	}

	/*
	 * Gets an array of the readable properties for an object
	 * 
	 * @return The array
	 */
	public String[] getGetablePropertyNames() {
		return readablePropertyNames;
	}

	/*
	 * Gets an array of the writeable properties for an object
	 * 
	 * @return The array
	 */
	public String[] getSetablePropertyNames() {
		return writeablePropertyNames;
	}

	/*
	 * Check to see if a class has a writeable property by name
	 * 
	 * @param propertyName - the name of the property to check
	 * 
	 * @return True if the object has a writeable property by the name
	 */
	public boolean hasSetter(
			String propertyName) {
		return setMethods.keySet()
				.contains(propertyName);
	}

	/*
	 * Check to see if a class has a readable property by name
	 * 
	 * @param propertyName - the name of the property to check
	 * 
	 * @return True if the object has a readable property by the name
	 */
	public boolean hasGetter(
			String propertyName) {
		return getMethods.keySet()
				.contains(propertyName);
	}

	public String findPropertyName(
			String name) {
		return caseInsensitivePropertyMap
				.get(name
						.toUpperCase(Locale.ENGLISH));
	}

	/*
	 * Gets an instance of ClassInfo for the specified class.
	 * 
	 * @param clazz The class for which to lookup the method cache.
	 * 
	 * @return The method cache for the class
	 */
	public static Reflector forClass(
			Class<?> clazz) {
		if (classCacheEnabled) {
			// synchronized (clazz) removed see issue #461
			Reflector cached = REFLECTOR_MAP
					.get(clazz);
			if (cached == null) {
				cached = new Reflector(
						clazz);
				REFLECTOR_MAP.put(
						clazz, cached);
			}
			return cached;
		} else {
			return new Reflector(clazz);
		}
	}

	public static void setClassCacheEnabled(
			boolean classCacheEnabled) {
		Reflector.classCacheEnabled = classCacheEnabled;
	}

	public static boolean isClassCacheEnabled() {
		return classCacheEnabled;
	}
}
